﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using DDD.Helper;

namespace DDD.Core {
    /// <summary>
    /// アニメーションを再生するコンポーネント
    /// </summary>
    /// <remarks>
    /// アニメーション プレイヤーはゲーム オブジェクトにアニメーションを再生する能力を付加するコンポーネントです｡
    /// 何らかのアニメーションを持つゲーム オブジェクトは必ずこのコンポーネントをアタッチされています。
    /// <see cref="AnimationPlayer"/> はアニメーションの起点となるクラスです。
    /// <para>
    /// <h3>アニメーションとクリップ</h3>
    /// アニメーションクリップは”歩く””走る”などの一連の動作をまとめたアニメーションの基本単位です。
    /// アニメーションはクリップ単位で追加や削除、または制御します。クリップの詳細については、、
    /// <see cref="AnimationClip"/> の解説を参照してください。
    /// <see cref="AnimationPlayer"/> はクリップを保存し、ゲーム オブジェクトにアニメーション機能を追加するコンポーネント クラスです。
    /// プレイヤーでは、<see cref="AnimationPlayer.Animate()"/> と <see cref="AnimationPlayer.Raise()"/> の2つのメソッドが最も重要です。
    /// </para>
    /// <para>
    /// <h3>Animate() メソッド</h3>
    /// <see cref="AnimationPlayer.Animate()"/> メソッドは登録されたアニメーション クリップから、指定のワールド時間の値を計算し、
    /// 指定のターゲット オブジェクトのプロパティを書き換えます。結果としてキャラクターは歩いたり走ったりするようになります。
    /// 通常このメソッドを1フレームに1回呼び出しますが、必ず呼び出す必要はありません。
    /// クリップの詳細については、<see cref="AnimationClip"/> を参照してください。
    /// </para>
    /// <para>
    /// <h3>Raise() メソッド</h3>
    /// <see cref="AnimationPlayer.Raise()"/> メソッドは登録されたアニメーション クリップ（の持つアニメーション トラック）に
    /// 設定されたアニメーション イベントを発火します（登録済みのイベント ハンドラーが呼ばれます）。
    /// 上記 <see cref="AnimationPlayer.Animate()"/> メソッドはイベントを発火しない事に注意してください。
    /// 原則としてDDDのデザインでは1つのメソッドが2つの動作をする事はありません。
    /// アニメーション イベントの詳細については、<see cref="AnimationEvnet"/> を参照してください。
    /// </para>
    /// </remarks>
    /// <seealso cref="AnimationClip"/>
    /// <seealso cref="AnimationTrack"/>
    /// <seealso cref="AnimationEvent"/>
    /// <seealso cref="AnimationEventHandler"/>
    public class AnimationPlayer : Component {
        #region Field
        List<AnimationClip> clips;
        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        /// <remarks>
        /// 規定の <see cref="AnimationPlayer"/> オブジェクトを作成します。
        /// クリップは何も登録されていません。
        /// </remarks>
        public AnimationPlayer () {
            this.clips = new List<AnimationClip> ();
        }
        #endregion

        #region Property
        /// <summary>
        /// クリップ数
        /// </summary>
        /// <remarks>
        /// このプレイヤーによって管理されるクリップの数です
        /// </remarks>
        public int ClipCount {
            get { return clips.Count; }
        }

        /// <summary>
        /// クリップを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// このプレイヤーによって制御されるクリップを列挙する列挙子
        /// </remarks>
        /// <seealso cref="AnimationClip"/>
        public IEnumerable<AnimationClip> Clips {
            get { return clips; }
        }
        #endregion

        #region Method

        /// <summary>
        /// クリップの追加
        /// </summary>
        /// <remarks>
        /// すでに追加済みのクリップを追加した場合無視します｡
        /// </remarks>
        /// <param name="clip">アニメーション クリップ</param>
        /// <exception cref="clip"><paramref name="clip"/> = <c>null</c></exception>
        /// <seealso cref="AnimationClip"/>
        public void AddClip (AnimationClip clip) {
            if (clip == null) {
                throw new ArgumentNullException ("Clip is null");
            }
            if (clips.Contains (clip)) {
                return;
            }
            this.clips.Add (clip);
        }

        /// <summary>
        /// クリップの削除
        /// </summary>
        /// <remarks>
        /// このプレイヤーから指定のクリップを取り除きます。
        /// このプレイヤーに登録されていないクリップを削除しようとした場合は安全に無視します｡
        /// <c>null</c> も無視されます。
        /// </remarks>
        /// <param name="clip">削除したいクリップ</param>
        /// <returns>削除したら <c>true</c>, そうでなければ <c>false</c></returns>
        /// <seealso cref="AnimationClip"/>
        public bool RemoveClip (AnimationClip clip) {
            return this.clips.Remove (clip);
        }

        /// <summary>
        /// クリップの取得
        /// </summary>
        /// <remarks>指定のインデックス <paramref name="index"/> のクリップを取得します｡</remarks>
        /// <param name="index">クリップ番号</param>
        /// <exception cref="IndexOutOfRangeException"><paramref name="index"/> &lt; 0 または <paramref name="index"/> &gt;= <see cref="ClipCount"/></exception>
        /// <returns>クリップ</returns>
        /// <seealso cref="AnimationClip"/>
        public AnimationClip GetClip (int index) {
            if (index < 0 || index >= clips.Count) {
                throw new IndexOutOfRangeException ("Index is out of range");
            }
            return clips[index];
        }

        /// <summary>
        /// クリップの取得
        /// </summary>
        /// <remarks>
        /// 指定の名前 <paramref name="name"/> と一致するクリップを取得します｡
        /// 複数一致する場合はそのうちのどれか1つを返します｡
        /// どれが返るかは未定義です｡
        /// </remarks>
        /// <param name="name">クリップの名前</param>
        /// <returns>該当するクリップ､または存在しない場合は <c>null</c></returns>
        /// <seealso cref="AnimationClip"/>
        public AnimationClip GetClip (string name) {
            return clips.Find (x => x.Name == name);
        }

        /// <summary>
        /// プロパティを更新します
        /// </summary>
        /// <remarks>
        /// 変更できるプロパティの型は<c>float,byte,sbyte,short,ushort,int,uint</c>の7種類か､
        /// もしくは<see cref="IAnimatable{T}"/>(ただしT=上記の7種類)インターフェースを実装した構造体です｡
        /// この以外の型のプロパティをアニメーションする事はできません｡
        /// 例えば配列はクラスなのでアニメーション対象にする事はできません｡
        /// (メモ) 
        /// <note type="implement">
        /// 配列は要素毎に[i]をつけて個別に指定する予定だったが、まだ実装していない｡
        /// </note>
        /// <note type="implement">
        /// これ <c>bool</c> 型を実装し忘れている。
        /// </note>
        /// </remarks>
        /// <param name="obj">変更対象のオブジェクト</param>
        /// <param name="pinfo">変更対象のプロパティ情報</param>
        /// <param name="newValue">新しい値</param>
        private void UpdateProperty (object obj, PropertyInfo pinfo, float[] newValue) {
            var value = pinfo.GetValue (obj, null);

            if (value is float) {
                var f = newValue[0];
                pinfo.SetValue (obj, f, null);
            }
            else if (value is byte) {
                var f = MyMath.ClampToByte (newValue[0]);
                pinfo.SetValue (obj, f, null);
            }
            else if (value is sbyte) {
                var f = MyMath.ClampToSByte (newValue[0]);
                pinfo.SetValue (obj, f, null);
            }
            else if (value is short) {
                var f = MyMath.ClampToShort (newValue[0]);
                pinfo.SetValue (obj, f, null);
            }
            else if (value is ushort) {
                var f = MyMath.ClampToUShort (newValue[0]);
                pinfo.SetValue (obj, f, null);
            }
            else if (value is int) {
                var f = MyMath.ClampToInt (newValue[0]);
                pinfo.SetValue (obj, f, null);
            }
            else if (value is uint) {
                var f = MyMath.ClampToUInt (newValue[0]);
                pinfo.SetValue (obj, f, null);
            }
            else if (value is IAnimatable<float>) {
                var f = value as IAnimatable<float>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = newValue[i];
                }
                pinfo.SetValue (obj, f, null);

            }
            else if (value is IAnimatable<byte>) {
                var f = value as IAnimatable<byte>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = MyMath.ClampToByte (newValue[i]);
                }
                pinfo.SetValue (obj, f, null);

            }
            else if (value is IAnimatable<sbyte>) {
                var f = value as IAnimatable<sbyte>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = MyMath.ClampToSByte (newValue[i]);
                }
                pinfo.SetValue (obj, f, null);
            }
            else if (value is IAnimatable<short>) {
                var f = value as IAnimatable<short>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = MyMath.ClampToShort (newValue[i]);
                }
                pinfo.SetValue (obj, f, null);
            }
            else if (value is IAnimatable<ushort>) {
                var f = value as IAnimatable<ushort>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = MyMath.ClampToUShort (newValue[i]);
                }
                pinfo.SetValue (obj, f, null);
            }
            else if (value is IAnimatable<int>) {
                var f = value as IAnimatable<int>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = MyMath.ClampToInt (newValue[i]);
                }
                pinfo.SetValue (obj, f, null);
            }
            else if (value is IAnimatable<uint>) {
                var f = value as IAnimatable<uint>;
                for (var i = 0; i < f.ComponentCount; i++) {
                    f[i] = MyMath.ClampToUInt (newValue[i]);
                }
                pinfo.SetValue (obj, f, null);
            }
            else {
                throw new NotImplementedException ("This type of property is not able to animate, type=" + pinfo.Name);
            }
        }

        /// <summary>
        /// アニメーションの実行
        /// </summary>
        /// <remarks>
        /// 登録済みのアニメーション クリップを使ってプロパティの値を更新します｡
        /// 通常引数の <paramref name="worldTime"/> には現在時刻を指定します｡
        /// このメソッドが呼ばれると DDD は以下の処理を行います。
        /// <code title="擬似コード">
        ///    一時的な値を0にする
        ///    foreach すべてのクリップ {
        ///      if 有効なら
        ///        foreach すべてのトラック {
        ///          キーフレーム シーケンスから指定時刻の値をサンプリング
        ///          ウェイト値を乗算して一時的な値に足す
        ///    }
        ///    ターゲットのプロパティ値を変更する
        /// </code>
        /// クリップは指定時刻でアクティブかつウェイト値が0より大きいものが有効です。
        /// アニメーションの詳細については TBD の解説を参照してください。
        /// このメソッドはプロパティの値を変更するのみでアニメーション イベントは発火しません｡
        /// イベントの発火は <see cref="Raise()"/> を使用してください。
        /// <see cref="Raise"/>を参照してください｡
        /// </remarks>
        /// <param name="worldTime">ワールド時間</param>
        /// <seealso cref="Raise()"/>
        public void Animate (int worldTime) {
            var trackerInfos = from clip in clips
                               where clip.IsActive (worldTime) && clip.Weight > 0
                               from tracker in clip.Trackers
                               select new { Tracker = tracker, Clip = clip };

            var query = from trackerInfo in trackerInfos
                        group trackerInfo by trackerInfo.Tracker.TrackTarget;

            foreach (var group in query) {
                var obj = group.Key.Object;
                var propName = group.Key.PropertyName;
                var propInfo = obj.GetType ().GetProperty (propName);
                var value = new float[16];
                var tmp = new float[16];

                foreach (var trackerInfo in group) {
                    var track = trackerInfo.Tracker.SourceTrack;
                    var clip = trackerInfo.Clip;
                    var pos = clip.GetPosition (worldTime);
                    var weight = clip.Weight;

                    track.Sample (pos, tmp);
                    for (var i = 0; i < track.ComponentCount; i++) {
                        value[i] += tmp[i] * weight;
                    }
                }

                UpdateProperty (obj, propInfo, value);
            }


        }

        /// <summary>
        /// アニメーション イベントの発火
        /// </summary>
        /// <remarks>
        /// このクリップが制御するクリップに設定されたイベントで、
        /// 指定時刻[<paramref name="startTime"/>,<paramref name="endTime"/>)に
        /// 再生されるものを全て発火させます｡
        /// イベントの発火とは設定されたイベントハンドラー メソッドを呼び出す事を指します。
        /// 通常 <paramref name="startTime"/> には現在時刻を､ <paramref name="endTime"/> には
        /// 次のフレームの時刻を指定します｡
        /// イベントは必ず <paramref name="startTime"/> に近い方から順に実行されます｡
        /// 同時刻のイベントの順番は未定義です。
        /// </remarks>
        /// <param name="startTime">イベント開始時刻</param>
        /// <param name="endTime">イベント終了時刻</param>
        /// <returns>発火したイベントの個数</returns>
        public int Raise (int startTime, int endTime) {

            var events = from clip in clips
                         where clip.Weight > 0
                         let start = clip.ActiveInterval.GetActiveStart (startTime)
                         let end = clip.ActiveInterval.GetActiveEnd (endTime)
                         where start <= end
                         let startPosition = clip.GetPosition (start)
                         let endPosition = clip.GetPosition (end)
                         from tracker in clip.Trackers
                         select tracker.SourceTrack.GetRaisingEvents (startPosition, endPosition) into raisedEvents
                         from ev in raisedEvents
                         orderby ev.Position
                         select ev;

            foreach (var ev in events) {
                ev.Handler (this, ev.Args);
            }

            return events.Count ();
        }

        #endregion
    }
}
